package org.classloaders;

import java.io.*;

/**
 * A CompilingClassLoader compiles your Java source on-the-fly. It
 * checks for nonexistent .class files, or .class files that are older
 * than their corresponding source code.
 *
 * @author OleR
 */
public class CompilingClassLoader extends ClassLoader {

    private static final String JAVAFILES_PATH = "src/main/java/";
    private static final String CLASSFILES_PATH = "target/classes/";

    private static final String JAVAC_CMD = "javac -d ";
    private static final String JAVA_EXT = ".java";
    private static final String CLASS_EXT = ".class";

    private static final String MSG_JAVA_FILE_NAME = "JavaFileName = ";
    private static final String MSG_CLASS_FILE_NAME = "ClassFileName = ";
    private static final String MSG_JAVA_NOT_EXISTS = ".java not exists!";
    private static final String MSG_COMPILE_FAILED = "Compile failed: ";


    /**
     * Given a filename, read the entirety of that file from disk
     * and return it as a byte array.
     *
     * @param filename relative path to class-file
     * @return file byte array
     * @throws IOException
     */
    private byte[] getBytes(String filename) throws IOException {

        File file = new File(filename);
        long len = file.length();
        byte raw[] = new byte[(int) len];

        try (FileInputStream fin = new FileInputStream(file)) {

            int r = fin.read(raw);
            if (r != len) {
                throw new IOException("Can't read all, " + r + " != " + len);
            }
        }

        return raw;
    }

    /**
     * Spawn a process to compile the java source code file
     * specified in the 'javaFile' parameter. Return a true if
     * the compilation worked, false otherwise.
     *
     * @param javaFile relative path to java file
     * @return compilation successful
     * @throws IOException
     */
    private boolean compile(String javaFile) throws IOException {

        System.out.println("CCL: Compiling " + javaFile + "...");

        Process p = Runtime.getRuntime().exec(JAVAC_CMD + CLASSFILES_PATH + " " + javaFile);
        try {
            p.waitFor();
        } catch (InterruptedException ie) {
            System.out.println(ie);
        }

        return p.exitValue() == 0;
    }

    /**
     * Automatically compiles source as necessary when looking for class files.
     *
     * @param name full class name
     * @param resolve does class have to be resolved
     * @return metaclass of our class
     * @throws ClassNotFoundException
     */
    public Class loadClass(String name, boolean resolve)
            throws ClassNotFoundException {

        Class clas = findLoadedClass(name);

        String fileStub = name.replace('.', '/');

        String javaFilename = JAVAFILES_PATH + fileStub + JAVA_EXT;
        String classFilename = CLASSFILES_PATH + fileStub + CLASS_EXT;
        File javaFile = new File(javaFilename);
        File classFile = new File(classFilename);

        System.out.println(MSG_JAVA_FILE_NAME + javaFilename);
        System.out.println(MSG_CLASS_FILE_NAME + classFilename);

        boolean javaExists = javaFile.exists();
        boolean classExists = classFile.exists();
        boolean lastModifiedOk = javaFile.lastModified() > classFile.lastModified();

        if (!javaExists) {
            System.out.println(MSG_JAVA_NOT_EXISTS);
        }

        if (javaExists && (!classExists || lastModifiedOk)) {
            try {
                if (!compile(javaFilename) || !classFile.exists()) {
                    throw new ClassNotFoundException(MSG_COMPILE_FAILED + javaFilename);
                }
            } catch (IOException ie) {
                throw new ClassNotFoundException(ie.toString());
            }
        }

        try {
            byte raw[] = getBytes(classFilename);
            clas = defineClass(name, raw, 0, raw.length);
        } catch (IOException ie) {
        }

        if (clas == null) {
            clas = findSystemClass(name);
        }

        if (resolve && clas != null) {
            resolveClass(clas);
        }
        if (clas == null) {
            throw new ClassNotFoundException(name);
        }

        return clas;
    }
}